From 3f110840b3f5274a54646f350328e1b7f5990f34 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 22 Jul 2014 08:06:36 -0700 Subject: [PATCH] Add a cargo-doc command --- Cargo.toml | 4 + src/bin/cargo-doc.rs | 62 +++++++ src/bin/cargo.rs | 1 + src/cargo/core/manifest.rs | 25 ++- src/cargo/ops/cargo_clean.rs | 33 ++-- src/cargo/ops/cargo_compile.rs | 6 +- src/cargo/ops/cargo_doc.rs | 22 +++ src/cargo/ops/cargo_rustc/context.rs | 6 +- src/cargo/ops/cargo_rustc/fingerprint.rs | 1 + src/cargo/ops/cargo_rustc/layout.rs | 2 + src/cargo/ops/cargo_rustc/mod.rs | 46 +++++- src/cargo/ops/mod.rs | 2 + src/cargo/util/toml.rs | 16 +- tests/test_cargo_doc.rs | 202 +++++++++++++++++++++++ tests/tests.rs | 1 + 15 files changed, 408 insertions(+), 21 deletions(-) create mode 100644 src/bin/cargo-doc.rs create mode 100644 src/cargo/ops/cargo_doc.rs create mode 100644 tests/test_cargo_doc.rs diff --git a/Cargo.toml b/Cargo.toml index e13a0ee62..e1f1d190d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,5 +67,9 @@ test = false name = "cargo-new" test = false +[[bin]] +name = "cargo-doc" +test = false + [[test]] name = "tests" diff --git a/src/bin/cargo-doc.rs b/src/bin/cargo-doc.rs new file mode 100644 index 000000000..e61c0dd90 --- /dev/null +++ b/src/bin/cargo-doc.rs @@ -0,0 +1,62 @@ +#![feature(phase)] + +#[phase(plugin, link)] +extern crate cargo; +extern crate serialize; + +#[phase(plugin, link)] +extern crate hammer; + +use std::os; + +use cargo::ops; +use cargo::{execute_main_without_stdin}; +use cargo::core::{MultiShell}; +use cargo::util::{CliResult, CliError}; +use cargo::util::important_paths::find_project_manifest; + +#[deriving(PartialEq,Clone,Decodable)] +struct Options { + manifest_path: Option, + jobs: Option, + update: bool, + no_deps: bool, +} + +hammer_config!(Options "Build the package's documentation", |c| { + c.short("jobs", 'j').short("update", 'u') +}) + +fn main() { + execute_main_without_stdin(execute); +} + +fn execute(options: Options, shell: &mut MultiShell) -> CliResult> { + let root = match options.manifest_path { + Some(path) => Path::new(path), + None => try!(find_project_manifest(&os::getcwd(), "Cargo.toml") + .map_err(|_| { + CliError::new("Could not find Cargo.toml in this \ + directory or any parent directory", + 102) + })) + }; + + let mut doc_opts = ops::DocOptions { + all: !options.no_deps, + compile_opts: ops::CompileOptions { + update: options.update, + env: if options.no_deps {"doc"} else {"doc-all"}, + shell: shell, + jobs: options.jobs, + target: None, + }, + }; + + try!(ops::doc(&root, &mut doc_opts).map_err(|err| { + CliError::from_boxed(err, 101) + })); + + Ok(None) +} + diff --git a/src/bin/cargo.rs b/src/bin/cargo.rs index 824daae8f..fe3a61aba 100644 --- a/src/bin/cargo.rs +++ b/src/bin/cargo.rs @@ -57,6 +57,7 @@ fn execute() { println!(" run # build and execute src/main.rs"); println!(" version # displays the version of cargo"); println!(" new # create a new cargo project"); + println!(" doc # build project's rustdoc documentation"); println!(""); diff --git a/src/cargo/core/manifest.rs b/src/cargo/core/manifest.rs index fa752fa07..afe616848 100644 --- a/src/cargo/core/manifest.rs +++ b/src/cargo/core/manifest.rs @@ -19,6 +19,7 @@ pub struct Manifest { authors: Vec, targets: Vec, target_dir: Path, + doc_dir: Path, sources: Vec, build: Vec, unused_keys: Vec, @@ -41,6 +42,7 @@ pub struct SerializedManifest { authors: Vec, targets: Vec, target_dir: String, + doc_dir: String, build: Option>, } @@ -55,6 +57,7 @@ impl> Encodable for Manifest { authors: self.authors.clone(), targets: self.targets.clone(), target_dir: self.target_dir.display().to_string(), + doc_dir: self.doc_dir.display().to_string(), build: if self.build.len() == 0 { None } else { Some(self.build.clone()) }, }.encode(s) } @@ -155,10 +158,25 @@ impl Profile { } } + pub fn default_doc() -> Profile { + Profile { + env: "doc".to_string(), + opt_level: 0, + debug: false, + test: false, + dest: Some("doc-build".to_string()), + plugin: false, + } + } + pub fn is_compile(&self) -> bool { self.env.as_slice() == "compile" } + pub fn is_doc(&self) -> bool { + self.env.as_slice() == "doc" + } + pub fn is_test(&self) -> bool { self.test } @@ -249,13 +267,14 @@ impl Show for Target { impl Manifest { pub fn new(summary: &Summary, targets: &[Target], - target_dir: &Path, sources: Vec, + target_dir: &Path, doc_dir: &Path, sources: Vec, build: Vec) -> Manifest { Manifest { summary: summary.clone(), authors: Vec::new(), targets: Vec::from_slice(targets), target_dir: target_dir.clone(), + doc_dir: doc_dir.clone(), sources: sources, build: build, unused_keys: Vec::new(), @@ -294,6 +313,10 @@ impl Manifest { &self.target_dir } + pub fn get_doc_dir(&self) -> &Path { + &self.doc_dir + } + pub fn get_source_ids(&self) -> &[SourceId] { self.sources.as_slice() } diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 35ec741f5..434bb65e2 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -1,25 +1,30 @@ use std::io::fs::{rmdir_recursive}; -use core::{SourceId}; + +use core::source::Source; +use sources::PathSource; use util::{CargoResult, human, ChainError}; -use ops::{read_manifest}; -use std::io::{File}; -use util::toml::{project_layout}; /// Cleans the project from build artifacts. pub fn clean(manifest_path: &Path) -> CargoResult<()> { - let mut file = try!(File::open(manifest_path)); - let data = try!(file.read_to_end()); - let layout = project_layout(&manifest_path.dir_path()); - let (manifest, _) = try!(read_manifest(data.as_slice(), - layout, - &SourceId::for_path(manifest_path))); + let mut src = PathSource::for_path(&manifest_path.dir_path()); + try!(src.update()); + let root = try!(src.get_root_package()); + let manifest = root.get_manifest(); let build_dir = manifest.get_target_dir(); - if build_dir.exists() { - rmdir_recursive(build_dir).chain_error(|| human("Could not remove build directory")) - } else { - Ok(()) + try!(rmdir_recursive(build_dir).chain_error(|| { + human("Could not remove build directory") + })) } + + let doc_dir = manifest.get_doc_dir(); + if doc_dir.exists() { + try!(rmdir_recursive(doc_dir).chain_error(|| { + human("Could not remove documentation directory") + })) + } + + Ok(()) } diff --git a/src/cargo/ops/cargo_compile.rs b/src/cargo/ops/cargo_compile.rs index 966848d44..d746901f4 100644 --- a/src/cargo/ops/cargo_compile.rs +++ b/src/cargo/ops/cargo_compile.rs @@ -85,7 +85,11 @@ pub fn compile(manifest_path: &Path, debug!("packages={}", packages); let targets = package.get_targets().iter().filter(|target| { - target.get_profile().get_env() == env + match env { + // doc-all == document everything, so look for doc targets + "doc" | "doc-all" => target.get_profile().get_env() == "doc", + env => target.get_profile().get_env() == env, + } }).collect::>(); let mut config = try!(Config::new(*shell, update, jobs, target)); diff --git a/src/cargo/ops/cargo_doc.rs b/src/cargo/ops/cargo_doc.rs new file mode 100644 index 000000000..f59bf9b04 --- /dev/null +++ b/src/cargo/ops/cargo_doc.rs @@ -0,0 +1,22 @@ +use std::io::fs; + +use ops; +use util::CargoResult; +use core::source::Source; +use sources::PathSource; + +pub struct DocOptions<'a> { + pub all: bool, + pub compile_opts: ops::CompileOptions<'a>, +} + +pub fn doc(manifest_path: &Path, + options: &mut DocOptions) -> CargoResult<()> { + let mut src = PathSource::for_path(&manifest_path.dir_path()); + try!(src.update()); + let root = try!(src.get_root_package()); + let output = root.get_manifest().get_target_dir().join("doc"); + let _ = fs::rmdir_recursive(&output); + try!(ops::compile(manifest_path, &mut options.compile_opts)); + Ok(()) +} diff --git a/src/cargo/ops/cargo_rustc/context.rs b/src/cargo/ops/cargo_rustc/context.rs index 8ccd14bc7..c96278147 100644 --- a/src/cargo/ops/cargo_rustc/context.rs +++ b/src/cargo/ops/cargo_rustc/context.rs @@ -216,7 +216,11 @@ impl<'a, 'b> Context<'a, 'b> { pub fn is_relevant_target(&self, target: &Target) -> bool { target.is_lib() && match self.env { - "test" => target.get_profile().is_compile(), + "doc" | "test" => target.get_profile().is_compile(), + // doc-all == document everything, so look for doc targets and + // compile targets in dependencies + "doc-all" => target.get_profile().is_compile() || + target.get_profile().is_doc(), _ => target.get_profile().get_env() == self.env, } } diff --git a/src/cargo/ops/cargo_rustc/fingerprint.rs b/src/cargo/ops/cargo_rustc/fingerprint.rs index 6d2c0f3d9..81520a904 100644 --- a/src/cargo/ops/cargo_rustc/fingerprint.rs +++ b/src/cargo/ops/cargo_rustc/fingerprint.rs @@ -56,6 +56,7 @@ pub fn prepare(cx: &mut Context, pkg: &Package, } for &target in targets.iter() { + if target.get_profile().is_doc() { continue } let layout = cx.layout(target.get_profile().is_plugin()); for filename in cx.target_filenames(target).iter() { let filename = filename.as_slice(); diff --git a/src/cargo/ops/cargo_rustc/layout.rs b/src/cargo/ops/cargo_rustc/layout.rs index 42bf4a0f8..8c06656ba 100644 --- a/src/cargo/ops/cargo_rustc/layout.rs +++ b/src/cargo/ops/cargo_rustc/layout.rs @@ -153,4 +153,6 @@ impl<'a> LayoutProxy<'a> { pub fn old_native(&self, pkg: &Package) -> Path { self.root.old_native(pkg) } + + pub fn proxy(&self) -> &'a Layout { self.root } } diff --git a/src/cargo/ops/cargo_rustc/mod.rs b/src/cargo/ops/cargo_rustc/mod.rs index c215c181a..9c5535b15 100644 --- a/src/cargo/ops/cargo_rustc/mod.rs +++ b/src/cargo/ops/cargo_rustc/mod.rs @@ -110,8 +110,12 @@ fn compile<'a, 'b>(targets: &[&'a Target], pkg: &'a Package, // interdependencies. let (mut libs, mut bins) = (Vec::new(), Vec::new()); for &target in targets.iter() { - let req = cx.get_requirement(pkg, target); - let jobs = rustc(pkg, target, cx, req); + let jobs = if target.get_profile().is_doc() { + vec![rustdoc(pkg, target, cx)] + } else { + let req = cx.get_requirement(pkg, target); + rustc(pkg, target, cx, req) + }; if target.is_lib() { libs.push_all_move(jobs); } else { @@ -222,6 +226,44 @@ fn prepare_rustc(package: &Package, target: &Target, crate_types: Vec<&str>, } } + +fn rustdoc(package: &Package, target: &Target, cx: &mut Context) -> Job { + // Can't document binaries, but they have a doc target listed so we can + // build documentation of dependencies even when `cargo doc` is run. + if target.is_bin() { + return Job::new(proc() Ok(Vec::new())) + } + + let pkg_root = package.get_root(); + let cx_root = cx.layout(false).proxy().dest().dir_path().join("doc"); + let rustdoc = util::process("rustdoc").cwd(pkg_root.clone()); + let rustdoc = rustdoc.arg(target.get_src_path()) + .arg("-o").arg(cx_root) + .arg("--crate-name").arg(target.get_name()); + let rustdoc = build_deps_args(rustdoc, target, package, cx, false); + + log!(5, "commands={}", rustdoc); + + let _ = cx.config.shell().verbose(|shell| { + shell.status("Running", rustdoc.to_string()) + }); + + let primary = cx.primary; + let name = package.get_name().to_string(); + Job::new(proc() { + if primary { + try!(rustdoc.exec().chain_error(|| { + human(format!("Could not document `{}`.", name)) + })) + } else { + try!(rustdoc.exec_with_output().and(Ok(())).map_err(|err| { + caused_human(format!("Could not document `{}`.\n{}", + name, err.output().unwrap()), err) + })) + } + Ok(Vec::new()) + }) +} fn build_base_args(mut cmd: ProcessBuilder, target: &Target, crate_types: &[&str]) -> ProcessBuilder { diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 5c16029fe..392b36421 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -4,6 +4,7 @@ pub use self::cargo_read_manifest::{read_manifest,read_package,read_packages}; pub use self::cargo_rustc::compile_targets; pub use self::cargo_run::run; pub use self::cargo_new::{new, NewOptions}; +pub use self::cargo_doc::{doc, DocOptions}; mod cargo_clean; mod cargo_compile; @@ -11,3 +12,4 @@ mod cargo_read_manifest; mod cargo_rustc; mod cargo_run; mod cargo_new; +mod cargo_doc; diff --git a/src/cargo/util/toml.rs b/src/cargo/util/toml.rs index 45dfacb1d..ad55d640a 100644 --- a/src/cargo/util/toml.rs +++ b/src/cargo/util/toml.rs @@ -234,6 +234,7 @@ fn inferred_lib_target(name: &str, layout: &Layout) -> Vec { path: Some(TomlPath(lib.clone())), test: None, plugin: None, + doc: None, }] }).unwrap_or(Vec::new()) } @@ -254,6 +255,7 @@ fn inferred_bin_targets(name: &str, layout: &Layout) -> Vec { path: Some(TomlPath(bin.clone())), test: None, plugin: None, + doc: None, } }) }).collect() @@ -268,6 +270,7 @@ fn inferred_example_targets(layout: &Layout) -> Vec { path: Some(TomlPath(ex.clone())), test: None, plugin: None, + doc: None, } }) }).collect() @@ -282,6 +285,7 @@ fn inferred_test_targets(layout: &Layout) -> Vec { path: Some(TomlPath(ex.clone())), test: None, plugin: None, + doc: None, } }) }).collect() @@ -316,6 +320,7 @@ impl TomlManifest { path: layout.lib.as_ref().map(|p| TomlPath(p.clone())), test: t.test, plugin: t.plugin, + doc: t.doc, } } else { t.clone() @@ -336,6 +341,7 @@ impl TomlManifest { path: bin.as_ref().map(|&p| TomlPath(p.clone())), test: t.test, plugin: None, + doc: t.doc, } } else { t.clone() @@ -387,6 +393,7 @@ impl TomlManifest { &summary, targets.as_slice(), &Path::new("target"), + &Path::new("doc"), sources, match project.build { Some(SingleBuildCommand(ref cmd)) => vec!(cmd.clone()), @@ -454,6 +461,7 @@ struct TomlTarget { crate_type: Option>, path: Option, test: Option, + doc: Option, plugin: Option, } @@ -491,8 +499,7 @@ fn normalize(libs: &[TomlLibTarget], enum TestDep { Needed, NotNeeded } - fn target_profiles(target: &TomlTarget, - dep: TestDep) -> Vec { + fn target_profiles(target: &TomlTarget, dep: TestDep) -> Vec { let mut ret = vec![Profile::default_dev(), Profile::default_release()]; match target.test { @@ -500,6 +507,11 @@ fn normalize(libs: &[TomlLibTarget], Some(false) => {} } + match target.doc { + Some(true) | None => ret.push(Profile::default_doc()), + Some(false) => {} + } + match dep { Needed => ret.push(Profile::default_test().test(false)), _ => {} diff --git a/tests/test_cargo_doc.rs b/tests/test_cargo_doc.rs new file mode 100644 index 000000000..6ca18abc4 --- /dev/null +++ b/tests/test_cargo_doc.rs @@ -0,0 +1,202 @@ +use support::{project, execs}; +use support::{COMPILING}; +use hamcrest::{assert_that, existing_file, existing_dir, is_not}; + +fn setup() { +} + +test!(simple { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + "#) + .file("src/lib.rs", r#" + pub fn foo() {} + "#); + + assert_that(p.cargo_process("cargo-doc"), + execs().with_status(0).with_stdout(format!("\ +{compiling} foo v0.0.1 (file:{dir}) +", + compiling = COMPILING, + dir = p.root().display()).as_slice())); + assert_that(&p.root().join("target/doc"), existing_dir()); + assert_that(&p.root().join("target/doc/foo/index.html"), existing_file()); +}) + +test!(no_build_main { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + "#) + .file("src/lib.rs", r#" + pub fn foo() {} + "#) + .file("src/main.rs", r#" + bad code + "#); + + assert_that(p.cargo_process("cargo-doc"), + execs().with_status(0).with_stdout(format!("\ +{compiling} foo v0.0.1 (file:{dir}) +", + compiling = COMPILING, + dir = p.root().display()).as_slice())); +}) + +test!(doc_no_libs { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + "#) + .file("src/main.rs", r#" + bad code + "#); + + assert_that(p.cargo_process("cargo-doc"), + execs().with_status(0)); +}) + +test!(doc_twice { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + "#) + .file("src/lib.rs", r#" + pub fn foo() {} + "#); + + assert_that(p.cargo_process("cargo-doc"), + execs().with_status(0).with_stdout(format!("\ +{compiling} foo v0.0.1 (file:{dir}) +", + compiling = COMPILING, + dir = p.root().display()).as_slice())); + + assert_that(p.cargo_process("cargo-doc"), + execs().with_status(0).with_stdout(format!("\ +{compiling} foo v0.0.1 (file:{dir}) +", + compiling = COMPILING, + dir = p.root().display()).as_slice())); +}) + +test!(doc_deps { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies.bar] + path = "bar" + "#) + .file("src/lib.rs", r#" + extern crate bar; + pub fn foo() {} + "#) + .file("bar/Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + "#) + .file("bar/src/lib.rs", r#" + pub fn bar() {} + "#); + + assert_that(p.cargo_process("cargo-doc"), + execs().with_status(0).with_stdout(format!("\ +{compiling} bar v0.0.1 (file:{dir}) +{compiling} foo v0.0.1 (file:{dir}) +", + compiling = COMPILING, + dir = p.root().display()).as_slice())); + + assert_that(&p.root().join("target/doc"), existing_dir()); + assert_that(&p.root().join("target/doc/foo/index.html"), existing_file()); + assert_that(&p.root().join("target/doc/bar/index.html"), existing_file()); +}) + +test!(doc_no_deps { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies.bar] + path = "bar" + "#) + .file("src/lib.rs", r#" + extern crate bar; + pub fn foo() {} + "#) + .file("bar/Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + "#) + .file("bar/src/lib.rs", r#" + pub fn bar() {} + "#); + + assert_that(p.cargo_process("cargo-doc").arg("--no-deps"), + execs().with_status(0).with_stdout(format!("\ +{compiling} bar v0.0.1 (file:{dir}) +{compiling} foo v0.0.1 (file:{dir}) +", + compiling = COMPILING, + dir = p.root().display()).as_slice())); + + assert_that(&p.root().join("target/doc"), existing_dir()); + assert_that(&p.root().join("target/doc/foo/index.html"), existing_file()); + assert_that(&p.root().join("target/doc/bar/index.html"), is_not(existing_file())); +}) + +test!(doc_only_bin { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies.bar] + path = "bar" + "#) + .file("src/main.rs", r#" + extern crate bar; + pub fn foo() {} + "#) + .file("bar/Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + "#) + .file("bar/src/lib.rs", r#" + pub fn bar() {} + "#); + + assert_that(p.cargo_process("cargo-doc"), + execs().with_status(0)); + + assert_that(&p.root().join("target/doc"), existing_dir()); + assert_that(&p.root().join("target/doc/bar/index.html"), existing_file()); +}) diff --git a/tests/tests.rs b/tests/tests.rs index a77d451f2..37a7364c6 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -31,3 +31,4 @@ mod test_cargo_run; mod test_cargo_version; mod test_cargo_new; mod test_cargo_compile_plugins; +mod test_cargo_doc; -- 2.30.2